Skip to content

fix(sandbox_web): guard Playwright fallback redirects#723

Open
PurpleCHOIms wants to merge 1 commit into
mainfrom
codex/propose-fix-for-ssrf-vulnerability-in-playwright
Open

fix(sandbox_web): guard Playwright fallback redirects#723
PurpleCHOIms wants to merge 1 commit into
mainfrom
codex/propose-fix-for-ssrf-vulnerability-in-playwright

Conversation

@PurpleCHOIms

Copy link
Copy Markdown
Member

Motivation

  • The Playwright/Chromium fallback followed attacker-controlled navigations without the curl transport's SSRF/per-hop RoE checks, allowing final browser URLs (loopback, RFC1918, metadata, etc.) to reach the agent.
  • Because the sandbox image now installs Chromium and the Playwright template deps by default, the browser tier became a reachable escalation path that must enforce the same safety checks as the curl grid.
  • The change aims to ensure the browser fallback either inherits the same SSRF safety and RoE gating or is suppressed when curl already blocked an unsafe redirect.

Description

  • Threaded a scope_check parameter into run_playwright_fallback and the fetch chain so per-hop RoE gating can be enforced for fallback runs.
  • After the Node template returns, executor.run_playwright_fallback now classifies the finalUrl via safety.classify_url and rejects/block-returns when the final URL is private/blocked or out of RoE scope instead of trusting the browser output.
  • fetch_chain prevents invoking the browser fallback if the curl transport trace already recorded an ssrf_redirect_blocked: error to avoid retrying an attacker-controlled URL with the browser tier.
  • Playwright templates (desktop + mobile) now install a request-interception safety guard that resolves and classifies every request (including NAT64/unwrapped IPv4-mapped IPv6 handling via ipaddr.js) and aborts requests that resolve to private/loopback/link-local/reserved or fail DNS resolution; ipaddr.js was added to the templates' package.json.
  • Added unit tests covering the new behaviors: blocking a private final URL and blocking an out-of-scope final URL, while preserving successful fallback behavior.

Testing

  • Ran the sandbox_web unit tests with the node subprocess mocked: DECEPTICON_SKIP_BOOT=1 pytest packages/decepticon/tests/unit/sandbox_web/test_executor.py packages/decepticon/tests/unit/sandbox_web/test_fetch_chain.py packages/decepticon/tests/unit/sandbox_web/test_safety.py and observed all tests pass (20 passed, 1 warning).
  • Performed Node syntax checks: node --check on both Playwright templates and python -m py_compile on modified Python modules, both succeeded with no syntax errors.
  • Verified that template dependency changes were limited to adding ipaddr.js and that git diff --check produced no whitespace or trailing-eol issues.

Codex Task

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.

profile_id="unknown_challenge",
success_selectors=["article#c"],
force_executor="playwright_real_chrome",
scope_check=lambda candidate: "example.com" in candidate,

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 24331aac78

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

# otherwise follow that redirect outside the curl SSRF guard.
if any((a.error or "").startswith("ssrf_redirect_blocked:") for a in trace):
skip_browser = True
stop_reason = stop_reason or "ssrf_redirect_blocked"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Mark SSRF redirect blocks as terminal

When any curl attempt records ssrf_redirect_blocked: after the grid later sets stop_reason to "exhausted" or "budget", this assignment keeps that existing value, so _give_up() still reports normal exhaustion and advertises playwright_mcp as an untried escalation. In the exact scenario this guard is trying to suppress, the caller can be told to retry the attacker-controlled redirect in an agent browser; treat the SSRF redirect block as the terminal stop reason instead of only filling it when empty.

Useful? React with 👍 / 👎.

att.error = f"ssrf_blocked:{reason}"
att.url = final_url
return att, ""
if scope_check is not None and not scope_check(final_url):

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Gate Playwright redirects before navigation

This RoE check runs only after _run_node_template() has already allowed Chromium to navigate and return the final page. With scope_check set, an in-scope URL that redirects to an out-of-scope public host will still be fetched by the browser before being marked blocked here, unlike the curl path's pre-hop gate; the scope decision needs to be enforced inside the Playwright route before route.continue().

Useful? React with 👍 / 👎.

# template runs without xvfb; set INSANE_HEADLESS=0 (with xvfb present)
# to run headful, which evades headless-detecting WAFs (Akamai/DataDome).
"headless": os.environ.get("INSANE_HEADLESS", "1") not in ("0", "false", "no"),
"allowPrivate": False,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Honor the private-network opt-in in Playwright

In environments that intentionally set INSANE_ALLOW_PRIVATE=1, the curl transport and the Python final-URL check use safety.allow_private_default(), but the browser template is always invoked with allowPrivate: false. That makes the new route guard abort loopback/RFC1918 requests before the Python-side opt-in can permit them, regressing local/private engagements that require Playwright fallback.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants